package com.wgo.bpot.common.transport.util;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.wgo.bpot.common.transport.exception.ConventionsException;


/**
 * Used to arrange for intelligent reflection.
 * 
 * {@url http://java.sun.com/docs/books/jls/third_edition/html/names.html#6.8.4}
 * 
 * @author petterei
 * @version $Id: MethodNamingConvention.java,v 1.1 2006-01-23 09:27:22 petterei Exp $
 */
public enum MethodNamingConvention {
	SERVER_METHOD("serverCall") {
		
	}
	,
	GET_METHOD("get") {
		@Override
		public Method retriveSetterMethod(Class targetClass, Method method) {
			String methodCoreName = method.getName().substring(getPrefix().length());
			if (0 < method.getParameterTypes().length) {
//				return findMethod(SET_METHOD.getPrefix(), methodCoreName, targetClass, method.getReturnType()); 
				return findMethod(SET_METHOD.getPrefix(), methodCoreName, targetClass, Collection.class); // if parameter then it must be an underlying collection..
			}
			return retrieveMethod(SET_METHOD.getPrefix(), methodCoreName, targetClass, method.getReturnType());
		}
		@Override
		public Method retriveGetterMethod(Class targetClass, Method method) {
			if (0 < method.getParameterTypes().length) {
//				return findMethod("", method.getName(), targetClass, method.getParameterTypes());
				return findMethod("", method.getName(), targetClass ); // parameter types, should nto be added - that would make no sense..  
			}
//			return method; //Not used to achieve consistent beaviour (should get same class-instance for get and set methods (not inteface-method and impl-method..).)
			return retrieveMethod("", method.getName(), targetClass);
		}
	}
	,
	SET_METHOD("set") {
		@Override
		public Method retriveSetterMethod(Class targetClass, Method method) {
			if (1 < method.getParameterTypes().length) {
//				return findMethod("", method.getName(), targetClass, method.getParameterTypes()[0]);
				return findMethod("", method.getName(), targetClass, Collection.class); // if parameter, then it must be an underlying collection.. This would be bad naming strategy, should use 'put' as prefix..
			}
//			return method; //Not used to achieve consistent beaviour (should get same class-instance for get and set methods (not inteface-method and impl-method..).)
			return retrieveMethod("", method.getName(), targetClass, method.getParameterTypes()[0]);
		}
		@Override
		public Method retriveGetterMethod(Class targetClass, Method method) {
			String methodCoreName = method.getName().substring(getPrefix().length());
			if (1 < method.getParameterTypes().length) {
//				return findMethod(GET_METHOD.getPrefix(), methodCoreName, targetClass);
				return findMethod(GET_METHOD.getPrefix(), methodCoreName, Collection.class); // if parameter then it must be an underlying collection.. This would be bad naming strategy, should use 'put' as prefix..
			}
			return retrieveMethod(GET_METHOD.getPrefix(), methodCoreName, targetClass);
		}
	}
	,
	ADD_METHOD ("add") {
		@Override
		public void validate(Method method) {
			super.validate(method);
			if (! method.getReturnType().equals(boolean.class)) {
				log.warning("Incompatible return type for add method " + method.getName() + " Is " + method.getReturnType().getSimpleName() + ", should be " + boolean.class.getSimpleName());
			}
		}
		@Override
		public Object retrieveDefaultReturnValue(Method method, Object... args) {
			return Boolean.TRUE;
		}
		@Override
		public Method retrieveOppositeMethod(Method method) throws ConventionsException {
			return internalRetrieveOppositeMethod(method, this, REMOVE_METHOD.getPrefix());
		}
		@Override
		public boolean hasValueChanged(Method method, Object invokeResult) {
			return ((Boolean)invokeResult).booleanValue();
		}
	}
	,
	REMOVE_METHOD ("remove"){
		@Override
		public void validate(Method method) {
			super.validate(method);
			ConventionsException retrieveOppositeMethodException = null;
			Method oppositeMethod = null;
			try {
				oppositeMethod = retrieveOppositeMethod(method);
			} catch (ConventionsException e) {
				retrieveOppositeMethodException = e;
			}
			if (null != oppositeMethod) {
				getMethodType(oppositeMethod).validate(method);
			} else {
				log.log(Level.WARNING,"Validation of " + getPrefix() + "-method ("+ method.getName()+") did not complete.",retrieveOppositeMethodException);
			}
		}
		@Override
		public Object retrieveDefaultReturnValue(Method method, Object... args) {
			return getMethodType(retrieveOppositeMethod(method)).retrieveDefaultReturnValue(method, args); //TODO check what happens when Object... is given as a Object... parameter 
		}
		@Override
		public boolean hasValueChanged(Method method, Object invokeResult) {
			ConventionsException retrieveOppositeMethodException = null;
			Method oppositeMethod = retrieveOppositeMethod(method);
			if (null != oppositeMethod) {
				return getMethodType(oppositeMethod).hasValueChanged(method,invokeResult);
			}
			log.log(Level.WARNING,getClass().getSimpleName() + ": Unable to determine if value has changed for " + getPrefix() + "-method ("+ method.getName()+").",retrieveOppositeMethodException);
			return true;
		}
		@Override
		public Method retrieveOppositeMethod(Method method) throws ConventionsException {
			Method oppositeMethod = null;
			String methodCoreName = method.getName().substring(getPrefix().length());
			Class declaringInterface = method.getDeclaringClass();
			Class[] paramTypes = method.getParameterTypes();
			try {
				oppositeMethod = retrieveMethod(ADD_METHOD.getPrefix(), methodCoreName, declaringInterface, paramTypes[0]);
			} catch (ConventionsException e) {}
			if (null == oppositeMethod) {
				try {
					oppositeMethod = retrieveMethod(PUT_METHOD.getPrefix(), methodCoreName, declaringInterface, paramTypes[0]);
				} catch (ConventionsException e) {}
				if (null == oppositeMethod) {
					try {
						oppositeMethod = retrieveMethod(MethodNamingConvention.INSERT_METHOD.getPrefix(), methodCoreName, declaringInterface, paramTypes[0]);
					} catch (ConventionsException e) {}
					if (null == oppositeMethod) {
						throw new ConventionsException(getClass() + ": Opposite method of " + method.getName() + " was not found."); 
					}
				}
			}
			return oppositeMethod;
		}
	}
	,
	PUT_METHOD ("put") {
		@Override
		public void validate(Method method) {
			super.validate(method);
			if (! method.getReturnType().equals(method.getParameterTypes()[0])) { //FIXME: parameter 0 or 1?
				log.warning("Incompatible return type for add method " + method.getName() + ". Is " + method.getReturnType().getSimpleName() + ", should be " + method.getParameterTypes()[0].getSimpleName());
			}
		}
		@Override
		public Object retrieveDefaultReturnValue(Method method, Object... args) {
			Object defaultReturnValue = null;
			if (null != args && 0 < args.length
					&& method.getReturnType().equals(args[0].getClass().getInterfaces()[0])) {
				defaultReturnValue = args[0];
			}
			return defaultReturnValue;
		}
		@Override
		public Method retrieveOppositeMethod(Method method) throws ConventionsException {
			return internalRetrieveOppositeMethod(method, this, REMOVE_METHOD.getPrefix());
		}
		@Override
		public boolean hasValueChanged(Method method, Object invokeResult) {
			boolean hasValueChanged = null != invokeResult;
			if (hasValueChanged) {
				Assert.warnIfFalse(method.getReturnType().equals(invokeResult.getClass().getInterfaces()[0]), getClass().getSimpleName() + ": Invalid return type for method " + method.getName() + ". (Was:" + invokeResult.getClass().getInterfaces()[0] + ")");
			}
			return hasValueChanged;
		}
	}
	,
	IS_METHOD ("is"){
		@Override
		public Method retriveSetterMethod(Class targetClass, Method method) {
			String methodCoreName = method.getName().substring(getPrefix().length());
			return retrieveMethod(SET_METHOD.getPrefix(), methodCoreName, targetClass, method.getReturnType());
		}
		@Override
		public Method retriveGetterMethod(@SuppressWarnings("unused") Class targetClass, Method method) {
			if (0 < method.getParameterTypes().length) {
				return findMethod("", method.getName(), targetClass);
			}
//			return method; //Not used to achieve consistent beaviour (should get same class-instance for get and set methods (not inteface-method and impl-method..).)
			return retrieveMethod("", method.getName(), targetClass);
		}
	}
	,
	HAS_METHOD ("has"){
		@Override
		public Method retriveSetterMethod(Class targetClass, Method method) {
			String methodCoreName = method.getName().substring(0,1).toUpperCase() + method.getName().substring(1);
			return retrieveMethod(SET_METHOD.getPrefix(), methodCoreName, targetClass, method.getReturnType());
		}
		@Override
		public Method retriveGetterMethod(@SuppressWarnings("unused") Class targetClass, Method method) {
			if (0 < method.getParameterTypes().length) {
				return findMethod("", method.getName(), targetClass);
			}
//			return method; //Not used to achieve consistent beaviour (should get same class-instance for get and set methods (not inteface-method and impl-method..).)
			return retrieveMethod("", method.getName(), targetClass);
		}
	}
	,
	INSERT_METHOD ("insert"){
	}
	;
	public static final Set<MethodNamingConvention> MODIFICATION_METHOD_TYPES;
	static {
		Set<MethodNamingConvention> set = new HashSet<MethodNamingConvention>(4);
		set.add(REMOVE_METHOD);
		set.add(SET_METHOD);
		set.add(ADD_METHOD);
		set.add(PUT_METHOD);
		set.add(INSERT_METHOD);
		MODIFICATION_METHOD_TYPES = set;
	}
	
	private static final Logger log = Logger.getLogger(MethodNamingConvention.class.getSimpleName());

	private String prefix;
	/**
	 * The super-interface of all concepts handled by the proxy. Used for 
	 * validation checking.
	 */
	public static Class PROXY_SUPER_INTERFACE = null;
	
	private MethodNamingConvention(String prefix) {
		this.prefix = prefix;
	}

	public String getPrefix() {
		return prefix;
	}

	public Method retriveSetterMethod(Class targetClass, Method method) {
		String methodPartialCoreName = method.getName().substring(getPrefix().length());
		return findMethod(SET_METHOD.getPrefix(), methodPartialCoreName, targetClass, Collection.class);
	}
	
	public Method retriveGetterMethod(Class targetClass, Method method) {
		String methodPartialCoreName = method.getName().substring(getPrefix().length());
		return findMethod(GET_METHOD.getPrefix(), methodPartialCoreName, targetClass);
	}
	
	/**
	 * Validate method according to convention. More specifically this applies
	 * for Maps, Sets, Lists and other collections. Hence ADD, PUT, INSERT and
	 * REMOVE. 
	 * 
	 * @param method
	 */
	public void validate(Method method) {
		
	}
	
	public static Method retrieveMethod(String methodPrefix, String methodCoreName, Class targetClass, Class... parameterType) { //EXTENSION: validate incoming method
		Method setterMethod = null;
		String methodName = methodPrefix + methodCoreName;
		try {
			setterMethod = targetClass.getMethod(methodName, parameterType);
		} catch(NoSuchMethodException e){
            // This is OK and should be handled by caller!
        } catch (Exception e) {
			String message = "Could not retrive "+methodPrefix+"-method";
			if (0 < parameterType.length) {
				message += " with parameter "+parameterType[0].getSimpleName();
			} else {
				message += " with no parameter";
			}
			message += " for core method-name: "+ methodName +", type " + targetClass.getSimpleName() ;
			throw new ConventionsException(message, e);
		}
		return setterMethod;
	}
	
	@SuppressWarnings("unchecked")
	public static Method findMethod(String methodPrefix, String partialMethodName, Class targetClass, Class... parameterType) { //EXTENSION: validate incoming method
		String methodNameStart = methodPrefix + partialMethodName;
		for (Method method : targetClass.getMethods()) {
			if (parameterType.length == method.getParameterTypes().length 
					&& method.getName().startsWith(methodNameStart) 
					&& (0 == parameterType.length || parameterType[0].isAssignableFrom(method.getParameterTypes()[0]))) {
				return method;
			}
		}
		String message = "Could not find "+methodPrefix+"-method for type " + targetClass.getSimpleName() + ", partial method-name " + methodNameStart + "*";
		if (0 < parameterType.length) {
			message += " (First parameter type: "+ parameterType[0].getSimpleName() +")";
		} else {
			message += " (No parameters)";
		}
		log.fine(message);
		return null;
	}
	
	public static MethodNamingConvention getMethodType(Method method) {
		for (MethodNamingConvention methodNameConvention : values()) {
			if (method.getName().startsWith(methodNameConvention.getPrefix())){
				return methodNameConvention;
			}
		}
		//throw new ConventionsException("No method type found for " + method.getName());
		return null;
	}

	public static boolean isLinkToConceptOrConcepts(Method method) {
		Class methodReturnType = method.getReturnType();
		return isLinkToConcept(methodReturnType) || isLinkToConcepts(methodReturnType);
	}
	
	public static boolean isActualLinkToConceptOrConcepts(Method method) {
		return isActualLinkToConcept(method) || isActualLinkToConcepts(method);
	}

	@SuppressWarnings("unchecked")
	public static boolean isLinkToConcept(Class returnType) {
		return null != returnType && PROXY_SUPER_INTERFACE.isAssignableFrom(returnType);
	}

	public static boolean isLinkToConcepts(Class returnType) {
		return null != returnType && Collection.class.isAssignableFrom(returnType); //TODO use intsanceof to optimize?
	}

	@SuppressWarnings("unchecked")
	public static boolean isMethodParameterOfConceptType(Class[] parameterTypes) { //EXTENSION: check all parameters?
		return ( 0 < parameterTypes.length && PROXY_SUPER_INTERFACE.isAssignableFrom(parameterTypes[0]));
	}

	public static boolean isActualLinkToConcept(Method method) {
		return 0 == method.getParameterTypes().length && isLinkToConcept(method.getReturnType());
	}

	public static boolean isActualLinkToConcepts(Method method) {
		return 0 == method.getParameterTypes().length && isLinkToConcepts(method.getReturnType());
	}

	/**
	 * For adding dirty methods, when value is modified otherwise (ie. through 
	 * constructor parameter)
	 * 
	 * @param args The methods actual parameters
	 * @return The a valid return value if the method is executed.
	 */
	public Object retrieveDefaultReturnValue(Method method, Object... args) {
		return null;
	}
	/**
	 * Method to find the opposite method if exists..
	 * 
	 * @param method The method to find the opposite.
	 * @return The logically opposite method.
	 */
	public Method retrieveOppositeMethod(Method method) throws ConventionsException {
		throw new InternalError(getClass().getName() + ": Method is not implemented.");
	}
	private static Method internalRetrieveOppositeMethod(Method method, MethodNamingConvention methodType, String oppositePrefix) {
		String methodCoreName = method.getName().substring(methodType.getPrefix().length());
		Class declaringInterface = method.getDeclaringClass();
		Class[] paramTypes = method.getParameterTypes();
		return retrieveMethod(oppositePrefix, methodCoreName, declaringInterface, paramTypes[0]);
	}
	/**
	 * TODO: document method
	 * 
	 * @param method
	 * @param invokeResult
	 * @return
	 */
	public boolean hasValueChanged(Method actualMethod, Object invokeResult) {
		return true;
	}
}
